/* 
 * [New BSD License (BSD 3-Clause License)]
 * Copyright (c) 2012, Max Bechtold
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 *     - Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     - Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     - Neither the name of the Rel2Xml Project nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
grammar Dml;

options {
  language = Java;
}

// Make sure Lexer also finds its way into right package
@lexer::header
{
/* 
 * [New BSD License (BSD 3-Clause License)]
 * Copyright (c) 2012, Max Bechtold
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 *     - Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     - Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     - Neither the name of the Rel2Xml Project nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
  package maxbe.rel2xml.grammar;
} 

@header {
/* 
 * [New BSD License (BSD 3-Clause License)]
 * Copyright (c) 2012, Max Bechtold
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 *     - Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     - Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     - Neither the name of the Rel2Xml Project nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
	package maxbe.rel2xml.grammar;
	
	import java.util.LinkedHashMap;
	
	import maxbe.rel2xml.generator.Column;
	import maxbe.rel2xml.generator.Table;
	import maxbe.rel2xml.generator.dml.*;
	import maxbe.rel2xml.generator.dml.predicates.*;
	import static maxbe.rel2xml.generator.dml.predicates.Predicate.*;
	
}

@members
{
  private QueryBuilder qb;
  
  private String rootNodePath;
}

dml_statement_list [QueryBuilder qb, String rootNodePath] returns [List<DmlInstance> dmls]
@init
{
  this.qb = qb;
  this.rootNodePath = rootNodePath;
  dmls = new ArrayList<DmlInstance>();
}
  :
  (d=dml_statement
                        { dmls.add($d.dml); }
  )*;

dml_statement returns [DmlInstance dml]
@init
{
  String name = null;
  qb.reset();
}
@after
{
  dml.setName(name);
}
  :
  ( '--' ID
                           { name = $ID.text; }
  )?
  ( s=select_statement
                           { dml = $s.statement; }
  | i=insert_statement
                           { dml = $i.statement; }
  | u=update_statement
                           { dml = $u.statement; }
  | d=delete_statement
                           { dml = $d.statement; }
  )
  SEMICOLON
  ;
  
table returns [Table t]
  :
  ID
              { $t = qb.sb.getTable($ID.text); }
  ;
  
select_statement returns [Query statement]
@init
{
  statement = new Query(rootNodePath);

  List<String> columnNames = null;
  List<ResultColumn> resolvedColumns = new ArrayList<ResultColumn>();
}
@after
{
  statement.setEntities(new LinkedHashMap<String, Entity>(qb.currentEntities));
  for (String columnName : columnNames)
      resolvedColumns.addAll(qb.resolveColumns(columnName));
  statement.setProjection(new Projection(resolvedColumns));
} 
  :
  SELECT
  cl=column_list[true]
                              { columnNames = $cl.cl; }
  f=from_clause
  (WHERE p=predicate
                              { statement.setPredicate($p.p); }
  )?
  (o=order_by_clause
						    							{ statement.setOrderBy($o.o); }
  )?
  ;
  
column_list[boolean allowWildcard] returns [List<String> cl]
@init
{
  $cl = new ArrayList<String>();
}
  :
  c=column_token[allowWildcard]
                                  { cl.add($c.col); }
  (COMMA
    c=column_token[allowWildcard]
                                  { cl.add($c.col); }
  )*
  ;
  
column_token[boolean allowWildcard] returns [String col]
@init
{
  col = "";
}
  :
  (t=ID DOT
                              { col += $t.text + '.'; }
  )?
  (c=ID
                              { col += $c.text; }
   | /*{allowWildcard}? =>*/ '*'
                              { col += '*'; }
  ) 
  ;
  
from_clause
  :
  FROM
  t=table_expression
                                { if ($t.alias != null) qb.useAliases = true;
                                  qb.addEntity($t.alias, $t.entity); }
  (COMMA t=table_expression
				                        { qb.addEntity($t.alias, $t.entity); }
  )*
  ;
  
table_expression returns [String alias, Entity entity]
  :
  ( t=table_token
                                { $alias = $t.a; $entity = new Entity($alias, $t.ta); }
  |
    LEFT_PAREN
    q=select_statement id=ID
    RIGHT_PAREN
                                { $alias = $id.text; $entity = new Entity($alias, $q.statement); }
  )
  ;

table_token returns [String a, Table ta]
  :
   t=ID
  (h=ID
                        { $a = $h.text; }
  )?
                        { $ta = qb.sb.getTable($t.text); }
  ;
  
/* Predicates need to be of the form A1 and ... and An and B
   where Ai is a join predicate and B is a conjunction comprising any additional predicates */
predicate returns [Conjunction p]
  :
  pr=conjunction
                                { $p = $pr.p; }
  ;


conjunction returns [Conjunction p]
@init
{
  Conjunction c = new Conjunction();
  $p = c;
}
  :
  d=disjunction
                              { c.addPredicate($d.p); }
  
  ( AND d=disjunction
	                            { c.addPredicate($d.p); }
	)*
  ;


disjunction returns [Predicate p]
  :
  ( a=atomic_predicate
                              { $p = $a.p; } 
  |                           { Disjunction d = new Disjunction();
                                $p = d; }
	  LEFT_PAREN
	  a=atomic_predicate
	                            { d.addPredicate($a.p); }
	  ( OR a=atomic_predicate
	                            { d.addPredicate($a.p); }
	  )+ RIGHT_PAREN
	)
  ;


atomic_predicate returns [Predicate p]
@init
{
  Comparison comparison = null;
}
  :
  (
    c=column_token[false]
    m=(EQ | UNEQ)
    v=(STRING | NUMBER | QMARK)
                                     { comparison = Comparison.parseComparison($m.text); }
	                                   { $p = new ColumnValueComparison(qb.resolveColumn($c.col), $v.text, comparison); }
  |
    c1=column_token[false]
    m=(EQ | UNEQ)
    c2=column_token[false]
                                     { comparison = Comparison.parseComparison($m.text);
                                        $p = new ColumnComparison(qb.resolveColumn($c1.col), qb.resolveColumn($c2.col), comparison); }
  |
                                     { boolean neg = false; }
    c=column_token[false] IS
    (NOT
                                     { neg = true; }
    )?
    NULL
                                     { $p = new NullCheck(qb.resolveColumn($c.col), neg); }
  )
  ;

column_value returns [String value, List<ResultColumn> colRefs]
@init
{
  $value = "";
  $colRefs = new ArrayList<ResultColumn>();
}
@after
{
  $value = $value.substring(0, $value.length() - 1);
}
  :
  ( s=(STRING | NUMBER
       | QMARK |NULL)
                                  { $value += $s.text + ' '; }
  | c=ID
                                  { $colRefs.add(qb.resolveColumn($c.text)); $value += "\%s "; }
  | v=(LEFT_PAREN
     | RIGHT_PAREN
     | '+' | '-' | '*' | '/'
     | '%' | '||')
                              { $value += $v.text + ' '; }
  )+
  ;

order_by_clause returns [OrderBy o]
@init
{
  $o = new OrderBy();
}
  :
  ORDERBY
  c=column_order
                                              { $o.addColumn($c.col); }
  (COMMA c=column_order
                                              { $o.addColumn($c.col); }
  )*
  
  ;

column_order returns [ResultColumn col]
@init
{
  boolean asc = true;
}
  :
  c=column_token[false]
                                            { col = qb.resolveColumn($c.col); }
  ( ASC
   | DESC
                                            { asc = false; }                  
  )?
                                            { col.setOrderedAscendant(asc); }
  ;
  
insert_statement returns [Insert statement]
@init
{
  $statement = new Insert(rootNodePath);
  List<String> columnNames = null;
  List<ResultColumn> columns = new ArrayList<ResultColumn>();
  List<String> values = new ArrayList<String>();
  Entity target = null;
}
@after
{
  if (columnNames == null)
    for (Column column : target.getTable().getColumns())
      columns.add(qb.resolveColumn(column.getName()));
  else
    for (String columnName : columnNames)
      columns.add(qb.resolveColumn(columnName));
        
  $statement.setInsertions(columns, values);
}
  :
  INSERT INTO
  t=table_token
                                            { target = new Entity($t.a, $t.ta); }
                                            { $statement.setTarget(target); }
                                            { qb.currentEntities.put(target.getAlias(), target); }
  (LEFT_PAREN
   cl=column_list[false]
                                            { columnNames = $cl.cl; }
   RIGHT_PAREN
  )?
  VALUES LEFT_PAREN
  v=(STRING | NUMBER | QMARK | NULL)
                                            { values.add($v.text); }
  (COMMA v=(STRING | NUMBER | QMARK | NULL) 
                                            { values.add($v.text); }
  )* RIGHT_PAREN
  ;

update_statement returns [Update statement]
@init
{
  $statement = new Update(rootNodePath);
}
  :
  UPDATE t=table_token
                                            { Entity target = new Entity($t.a, $t.ta); }
                                            { $statement.setTarget(target); }
                                            { qb.currentEntities.put(target.getAlias(), target); }
  SET a=assignment_list[$statement]
  ( WHERE p=predicate
                                            { $statement.setPredicate($p.p); }
  )?
  ;
  
assignment_list [Update update]
  :
  a=assignment
                                            { $update.addAssignment($a.a); }
  ( COMMA
    a=assignment
                                            { $update.addAssignment($a.a); }
  )*
  ;

assignment returns [Assignment a]
  :
  c=column_token[false] EQ v=column_value
                                            { $a = new Assignment(qb.resolveColumn($c.col), $v.value, $v.colRefs); }
  ;

delete_statement returns [Delete statement]
@init
{
  $statement = new Delete(rootNodePath);
}
  :
  DELETE FROM t=table_token
                                            { Entity target = new Entity($t.a, $t.ta); }
                                            { $statement.setTarget(target); }
                                            { qb.currentEntities.put(target.getAlias(), target); }
  (WHERE p=predicate
                                            { $statement.setPredicate($p.p); }
  )?
  ;
  
/*------------------------------------------------------------------
 * LEXER RULES
 *------------------------------------------------------------------*/

// Reserved SQL keywords

SELECT : ('select'|'Select'|'SELECT');
FROM : ('from'|'From'|'FROM');
WHERE : ('where'|'Where'|'WHERE');
ORDERBY : ('order by'|'Order By'|'ORDER BY');
ASC : ('asc'|'Asc'|'ASC');
DESC : ('desc'|'Desc'|'DESC');
AND : ('and'|'And'|'AND');
OR : ('or'|'Or'|'OR');
IS : ('is'|'Is'|'IS');
NOT : ('not'|'Not'|'NOT');
NULL: ('null'|'Null'|'NULL');
INSERT : ('insert'|'Insert'|'INSERT');
INTO : ('into'|'Into'|'INTO');
VALUES : ('values'|'Values'|'VALUES');
UPDATE : ('update'|'Update'|'UPDATE');
SET : ('set'|'Set'|'SET');
DELETE : ('delete'|'Delete'|'DELETE');

// Common symbols

LEFT_PAREN : '(';
RIGHT_PAREN : ')';
COMMA : ',';
SEMICOLON : ';';
DOT : '.';
NUMBER  : (DIGIT)+;
DATESTRING: (DIGIT | '-' | '_' | ':')+;
EQ : '=';
UNEQ : ( '<' | '<=' | '>' | '>=' | '<>' );
ID  : (('a'..'z'|'A'..'Z' | '_') ((DIGIT)*))+ ;
NEWLINE: '\r'? '\n' { $channel = HIDDEN; } ;
WS : ( '\t' | ' ' | '\r' | '\n' | '\u000C' )+ { $channel = HIDDEN; } ;
fragment DIGIT :   '0'..'9' ;
PERCENT : '%';
DOTDOT : '..';
QUOTE : '"';
STRING : QUOTE ('a'..'z'|'A'..'Z' | '-' | '_' | DIGIT | DOT | ':')+ QUOTE;
QMARK : '?';
MULTI_COMMENT options { greedy = false; } : '/*' .* '*/' NEWLINE? { skip(); };